#include "raycaster.h"
#include "icgobject.h"
#include "ilightsource.h"
#include <cmath>

RayCaster::RayCaster()
{
}

const CgColorBuffer* RayCaster::render(Scene *scene, int width, int height)
{
    Camera camera = scene->camera();
    ProjectionPlane projplane = camera.projection_plane();
    double dx = (projplane.right() - projplane.left()) / width;
    double dy = (projplane.top() - projplane.bottom()) / height;
    CgColorBuffer *buffer = new CgColorBuffer(width, height);

    applySceneTransformations(scene);
    for(int i = 0 ; i < height ; i++) {
        double row = projplane.top() - i*dy;
#pragma omp parallel for
        for(int j = 0 ; j < width; j++) {
            const Point grid_point(projplane.left() + j*dx, row, -projplane.distance_from_camera());
            const Vector ray_dir(grid_point - Point(0.0, 0.0, 0.0));
            Vector normal;
            double t = -1.0;
            CgMaterial material;
            Ray ray(Point(0.0, 0.0, 0.0), ray_dir);

            if( get_ray_collision(scene, ray, t, normal, material) ) {
                if( (ray_dir * normal) > 0 ) // Invert  normal if it doesnt points towards the observer
                    normal = -normal;

                buffer->setColor(i, j, get_point_color(scene, Point(0.0, 0.0, 0.0) + t*ray.dir(), normal, material));
            } else {
                buffer->setColor(i, j, scene->background_color());
            }
        }
    }

    return buffer;
}

bool RayCaster::get_ray_collision(const Scene *scene, const Ray &ray, double &t, Vector &normal, CgMaterial &material) const
{
    std::vector<ICgObject*> objects = scene->objects();
    std::vector<ICgObject*>::iterator it;
    double temp_t = -1.0;
    Vector temp_normal;
    Camera camera = scene->camera();

    t = INFINITY;
    for(it = objects.begin() ; it != objects.end() ; it++) {
        const ICgObject* obj = *it;
        if( obj->intersect(ray, temp_t, temp_normal) && (temp_t < t) ) {
            t = temp_t;
            normal = temp_normal;
            material = obj->material();
        }
    }

    return !isinf(t);
}

CgColor RayCaster::get_point_color(const Scene *scene, const Point &p, const Vector &normal, const CgMaterial &material) const
{
    CgColor result;
    std::vector<ILightSource*> lights = scene->light_sources();
    std::vector<ILightSource*>::const_iterator it;

    for(it = lights.begin() ; it != lights.end() ; it++) {
        const ILightSource *light = *it;
//        CgColor lightColor = light->intensity(p, Point(0.0, 0.0, 0.0), normal, material);
        CgColor lightColor = PhongShade(light, p, normal, material);
        result = result + lightColor;
    }
    result = result + (material.ambient() * scene->ambient());

    return result;
}

void RayCaster::applySceneTransformations(Scene *scene) const
{
    std::vector<ICgObject*> objects = scene->objects();
    std::vector<ILightSource*> lights = scene->light_sources();
    std::vector<ICgObject*>::iterator obj_it;
    std::vector<ILightSource*>::iterator light_it;
    Camera camera = scene->camera();
    Matrix camera_transformation = Matrix::coordChangeMatrix(camera.frame());

    for(obj_it = objects.begin() ; obj_it != objects.end() ; obj_it++) {
        ICgObject *obj = *obj_it;
        obj->multiplyCurrentTransformation(camera_transformation);
        obj->applyCurrentTransformation();
    }
    for(light_it = lights.begin() ; light_it != lights.end() ; light_it++) {
        ILightSource *light = *light_it;
        light->applyCameraTransformation(camera);
    }

}

CgColor RayCaster::PhongShade(const ILightSource *light, const Point &p, const Vector &normal, const CgMaterial &material) const
{
    CgColor ambient, diffuse, specular;
    Vector I = light->direction(p);
    I.normalize();
    const double diffuse_angle = I*normal;
    Vector r = 2*(diffuse_angle)*normal - I;
    Vector v = Point(0.0, 0.0, 0.0) - p;
    v.normalize();
    const double specular_exponent = 2.0;
    const double specular_atenuation = pow(r*v, specular_exponent);
    LightIntensity li = light->intensity(p);

    ambient = li.ambient() * material.ambient();
    diffuse = diffuse_angle * li.diffuse() * material.diffuse();
    specular = specular_atenuation * li.specular() * material.specular();

    return ambient + diffuse + specular;
}
